package away3d.entities


import away3d.bounds.AxisAlignedBoundingBox ;
import away3d.bounds.BoundingVolumeBase ;
import away3d.cameras.Camera3D ;
import away3d.containers.Scene3D ;
import away3d.errors.AbstractMethodError ;
import away3d.core.partition.EntityNode ;
import away3d.core.partition.Partition3D ;
import flash.geom.Matrix3D ;
import away3d.containers.ObjectContainer3D ;



/**
 * The Entity class provides an abstract base class for all scene graph objects that are considered having a
 * "presence" in the scene, in the sense that it can be considered an actual object with a position and a size ( even
 * if infinite or idealised ), rather than a grouping.
 * Entities can be partitioned in a space partitioning system and in turn collected by an EntityCollector.
 *
 * @see away3d.partition.Partition3D
 * @see away3d.core.traverse.EntityCollector
 */
class Entity extends ObjectContainer3D
{
    
    private var _partitionNode      : EntityNode ;
    private  var _mvpTransformStack : Vector<Matrix3D> ;
    private  var _zIndices          : Vector<Float> ;
    private  var _mvpIndex          : Int ;
    private  var _stackLen          : Int ;
    private  var _bounds            : BoundingVolumeBase ;
    private  var _boundsInvalid     : Bool ;
    private var _showBounds         : Bool ;

    private var _mouseEnabled : Bool ;

    /**
     * Creates a new Entity object.
     */
    public function new()
    {
        
        _mvpTransformStack      = new Vector<Matrix3D>() ;
        _zIndices               = new Vector<Float>() ;
        _mvpIndex               = -1 ;
        _boundsInvalid          = true ;
        
        super() ;
        _bounds = getDefaultBoundingVolume() ;
        
    }
    
    
    private function get_showBounds() : Bool
    {
        
        return _showBounds ;
        
    }
    
    
    private function set_showBounds( value : Bool )
    {
        
        if( value == _showBounds ) return ;
        
        _showBounds = value ;
        
        if( value ) 
        {
            
            addChild( _bounds.boundingRenderable ) ;
            
        }
        else
        {
            
            removeChild( _bounds.boundingRenderable ) ;
            _bounds.disposeRenderable() ;
            
        }
        
    }
    
    
    /**
     * Indicates whether the IRenderable should trigger mouse events, and hence should be rendered for hit testing.
     */
    private function get_mouseEnabled() : Bool
    {
        
        return _mouseEnabled ;
        
    }
    
    
    private function set_mouseEnabled( value : Bool )
    {
        
        _mouseEnabled = value ;
        
    }
    
    
    public var minX( get_minX, null ): Float ;
    
    
    /**
     * @inheritDoc
     */
    override private function get_minX() : Float
    {
        
        if( _boundsInvalid ) updateBounds() ;
        return _bounds.min.x ;
        
    }
    
    
    public var minY( get_minY, null ): Float ;
    
    
    /**
     * @inheritDoc
     */
    override private function get_minY() : Float
    {
        
        if( _boundsInvalid ) updateBounds() ;
        return _bounds.min.y ;
        
    }
    
    
    public var minZ( get_minZ, null ): Float ;
    
    
    /**
     * @inheritDoc
     */
    override private function get_minZ() : Float
    {
        
        if( _boundsInvalid ) updateBounds() ;
        return _bounds.min.z ;
        
    }
    
    
    public var maxX( get_maxX, null ): Float ;
    
    
    /**
     * @inheritDoc
     */
    override private function get_maxX() : Float
    {
        if( _boundsInvalid ) updateBounds() ;
        return _bounds.max.x ;
    }
    
    
    public var maxY( get_maxY, null ): Float ;
    
    
    /**
     * @inheritDoc
     */
    override private function get_maxY() : Float
    {
        if( _boundsInvalid ) updateBounds() ;
        return _bounds.max.y ;
    }
    
    
    public var maxZ( get_maxZ, null ): Float ;
    
    
    /**
     * @inheritDoc
     */
    override private function get_maxZ() : Float
    {
        if( _boundsInvalid ) updateBounds() ;
        return _bounds.max.z ;
    }
    
    
    public var bounds( get_bounds, set_bounds ) : BoundingVolumeBase ;
    
    
    /**
     * The bounding volume approximating the volume occupied by the Entity.
     */
    private function get_bounds() : BoundingVolumeBase
    {
        
        if( _boundsInvalid ) updateBounds() ;
        return _bounds ;
        
    }
    
    
    private function set_bounds( value : BoundingVolumeBase ) : BoundingVolumeBase
    {
        
        _bounds = value ;
        _boundsInvalid = true ;
        return value ;
        
    }
    
    
    /**
     * Updates the model-view-projection ( MVP ) matrix used to transform from model to homogeneous projection space
     * and places it on the stack. The stack allows nested rendering while keeping the MVP intact.
     * @param camera The camera which will perform the view transformation and projection.
     */
    public function pushModelViewProjection( camera : Camera3D )
    {
        
        if( ++_mvpIndex == _stackLen++ )
        {
            
            _mvpTransformStack[_mvpIndex] = new Matrix3D() ;
            
        }
        var mvp = _mvpTransformStack[_mvpIndex] ;
        mvp.copyFrom( sceneTransform ) ;
        mvp.append( camera.viewProjection ) ;
        mvp.copyRowTo( 3, _pos ) ;
        _zIndices[_mvpIndex] = -_pos.z ;
        
    }
    
    
    /**
     * The current model-view-projection ( MVP ) matrix - the one on the top of the stack - used to transform from
     * model to homogeneous projection space.
     */
    private function get_modelViewProjection() : Matrix3D
    {
        
        // assume base if popped
        return _mvpTransformStack[ ( ( _mvpIndex > 0 )*_mvpIndex )] ;
        
    }
    
    /**
     * Removes a model view projection matrix from the stack, used when leaving a render.
     */
    public function popModelViewProjection()
    {
        
        --_mvpIndex ;
        
    }
    
    
    public var zIndex( get_zIndex, set_zIndex ) : Float ; 
    
    
    /**
     * The distance of the IRenderable object to the view, used to sort per object.
     */
    private function get_zIndex() : Float
    {
        return _zIndices[ ( ( _mvpIndex > 0 )*_mvpIndex ) ] ;
    }
    
    
    public var EntityPartitionNode( get_EntityPartitionNode, set_EntityPartitionNode ): EntityNode ;
    
    
    /**
     * Gets a concrete EntityPartition3DNode subclass that is associated with this Entity instance
     */
    private function get_EntityPartitionNode() : EntityNode
    {
        
        if( _partitionNode == null )
        {
            
            _partitionNode = createEntityPartitionNode() ;
            
        }
        
        return _partitionNode ;
        
    }
    
    
    public var implicitPartition( null, set_implicitPartition ) : Partition3D ;
    
    
    /**
     * @inheritDoc
     */
    override public function set_implicitPartition( value : Partition3D ) : Partition3D
    {
        
        if( value == _implicitPartition ) return ;
        if( _implicitPartition ) notifyPartitionUnassigned() ;
        super.implicitPartition = value ;
        notifyPartitionAssigned() ;
        return value ;
        
    }
    
    
    public var scene( null, set_scene ) : Scene3D ;
    
    
    /**
     * @inheritDoc
     */
    override public function set_scene( value : Scene3D )
    {
        
        if( value == _scene ) return ;

        if( _scene ) _scene.unregisterEntity( this ) ;
        // callback to notify object has been spawned. Casts to please FDT
        if( value ) value.registerEntity( this ) ;

        super.scene = value ;
        return value ;
        
    }

    /**
     * Factory method that returns the current partition node. Needs to be overridden by concrete subclasses
     * such as Mesh to return the correct concrete subtype of EntityPartition3DNode ( for Mesh = MeshPartition3DNode,
     * most IRenderables ( particles fe ) would return RenderablePartition3DNode, I suppose )
     */
    private  function createEntityPartitionNode() : EntityNode
    {
        
        throw new AbstractMethodError() ;
        
    }
    
    
    /**
     * Creates the default bounding box to be used by this type of Entity.
     * @return
     */
    public function getDefaultBoundingVolume() : BoundingVolumeBase
    {
        
        // point lights should be using sphere bounds
        // directional lights should be using null bounds
        return new AxisAlignedBoundingBox() ;
        
    }
    
    
    /**
     * Updates the bounding volume for the object. Overriding methods need to set invalid flag to false!
     */
    public function updateBounds()
    {
        
        throw new AbstractMethodError() ;
        
    }
    
    
    /**
     * @inheritDoc
     */
    override public function invalidateSceneTransform()
    {
        
        super.invalidateSceneTransform() ;
        notifySceneBoundsInvalid() ;
        
    }
    
    
    /**
     * Invalidates the bounding volume, causing to be updated when requested.
     */
    public function invalidateBounds()
    {
        
        _boundsInvalid = true ;
        notifySceneBoundsInvalid() ;
        
    }
    
    
    /**
     * Notify the scene that the global scene bounds have changed, so it can be repartitioned.
     */
    private function notifySceneBoundsInvalid()
    {
        
        if( _scene ) _scene.invalidateEntityBounds( this ) ;
        
    }
    
    
    /**
     * Notify the scene that a new partition was assigned.
     */
    private function notifyPartitionAssigned()
    {
        
        if( _scene ) _scene.registerPartition( this ) ;//_onAssignPartitionCallback( this ) ;
        
    }
    
    
    /**
     * Notify the scene that a partition was unassigned.
     */
    private function notifyPartitionUnassigned()
    {
        
        if( _scene ) _scene.unregisterPartition( this ) ;
        
    }
    
    
}
}